//---------------------------------------------------------------------
// Author: Keith Hill, jachymko
//
// Description: Base class for commands which require a Path,  
//              LiteralPath and generic PSObject input parameters.
//
// Creation Date: Dec 23, 2006
//---------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;

namespace Pscx.Commands.Internal
{
    public class PscxInputObjectPathSettings
    {
        public PscxInputObjectPathSettings(bool fileAsPath, bool dirAsPath, bool recursive)
        {
            ProcessFileInfoAsPath = fileAsPath;
            ProcessDirectoryInfoAsPath = dirAsPath;
            ProcessIEnumerableRecursively = recursive;
        }

        public bool ProcessFileInfoAsPath;
        public bool ProcessDirectoryInfoAsPath;
        public bool ProcessIEnumerableRecursively;
    }

    public abstract class PscxInputObjectPathCommandBase : PscxPathCommandBase
    {
        protected const string ParameterSetObject = "Object";

        private PscxInputObjectPathSettings _settings;
        private Dictionary<Type, Delegate> _actions;
        private bool _hasInterfaceActions;
        private PSObject _inputObject;

        [Parameter(ParameterSetName = ParameterSetObject, Mandatory = true, ValueFromPipeline = true,
                   HelpMessage = "Accepts an object as input to the cmdlet. Enter a variable that contains the objects or type a command or expression that gets the objects.")]
        [AllowNull]
        [AllowEmptyString]
        public PSObject InputObject
        {
            get { return _inputObject; }
            set { _inputObject = value; }
        }

        protected PscxInputObjectPathCommandBase()
        {
            _settings = InputSettings;
        }

        protected virtual PscxInputObjectPathSettings InputSettings
        {
            get { return new PscxInputObjectPathSettings(true, true, true); }
        }

        protected virtual void ProcessNullInput()
        {
        }

        /// <summary>
        /// You shouldnt need to override this function in most cases. Just
        /// call RegisterInputType&lt;T&gt; with your delegate, and this method
        /// will take care of determining the input type and calling your delegate
        /// if appropriate.
        /// 
        /// Called when the InputParameter is specified or a pipeline object that isn't
        /// a string (those bind to Path or LiteralPath) is bound to this parameter.
        /// Note: This method will be called even when inputObject is null.
        /// </summary>
        /// <param name="inputObject"></param>
        protected virtual void ProcessInputObject(PSObject inputObject)
        {
            if (inputObject == null)
            {
                ProcessNullInput();
            }
            else
            {
                ProcessInputObjectInternal(inputObject.BaseObject);
            }
        }

        protected override void BeginProcessing()
        {
            base.BeginProcessing();

            if (_settings.ProcessFileInfoAsPath)
            {
                RegisterInputType<FileInfo>(delegate(FileInfo fileInfo)
                {
                    ProcessPath(fileInfo.FullName);
                });
            }

            if (_settings.ProcessDirectoryInfoAsPath)
            {
                RegisterInputType<DirectoryInfo>(delegate(DirectoryInfo dirInfo)
                {
                    ProcessPath(dirInfo.FullName);
                });
            }
        }

        protected override void ProcessRecord()
        {
            if (ParameterSetName == ParameterSetObject)
            {
                ProcessInputObject(_inputObject);
            }
            else
            {
                // Chain up to PscxPathCommandBase so that paths can be processed.
                base.ProcessRecord();
            }
        }

        /// <summary>
        /// Registers a delegate called when specified type of object
        /// is read from the pipeline.
        /// </summary>
        /// <remarks>
        /// You may register an interface type too, but they are processed only when
        /// there is no handler available for the actual type.
        /// </remarks>
        /// <example>
        /// <para>For example, if you register the following types, the IDisposable handler 
        /// will be executed only on objects which do not inherit from Stream.</para>
        /// <code>
        ///     RegisterInputType&lt;IDisposable&gt;(/* ... */);
        ///     RegisterInputType&lt;FileStream&gt;(/* ... */);
        ///     RegisterInputType&lt;Stream&gt;(/* ... */);
        /// </code>
        /// </example>
        /// <typeparam name="T">The type which should be processed. </typeparam>
        /// <param name="action">
        /// The delegate that is called when the object is available.
        /// </param>
        protected void RegisterInputType<T>(Action<T> action)
        {
            Throw.IfIsNull(action,
                "To ignore a input type, use IgnoreInputType<T>().");
            Throw.ArgumentOutOfRangeExceptionIf(typeof(T) == typeof(Object),
                "You cannot register an action for Object. Override the ProcessInputObject method instead.");
            Throw.ArgumentOutOfRangeExceptionIf(typeof(T) == typeof(PSObject),
                "You cannot register an action for PSObject. Override the ProcessInputObject method instead.");

            if (_settings.ProcessIEnumerableRecursively)
            {
                Throw.ArgumentOutOfRangeExceptionIf(typeof(T) == typeof(IEnumerable),
                    "You cannot register an action for IEnumerable when ProcessIEnumerableRecursively is true.");
            }

            SetTypeAction(typeof(T), action);
        }

        protected void IgnoreInputType<T>()
        {
            SetTypeAction(typeof(T), null);
        }

        private void SetTypeAction(Type type, Delegate action)
        {
            if (_actions == null)
            {
                _actions = new Dictionary<Type, Delegate>();
            }

            if (type.IsInterface)
            {
                _hasInterfaceActions = true;
            }

            _actions[type] = action;
        }

        private void ProcessInputObjectInternal(object target)
        {
            if (target == null)
            {
                ProcessNullInput();
                return;
            }

            if (_actions == null)
            {
                return;
            }

            Type type = target.GetType();

            do
            {
                if (TryInvokeAction(type, target))
                {
                    return;
                }

                type = type.BaseType;
            }
            while (type != null);

            if (_hasInterfaceActions)
            {
                foreach (Type interfaceType in target.GetType().GetInterfaces())
                {
                    if (TryInvokeAction(interfaceType, target))
                    {
                        return;
                    }
                }
            }

            IEnumerable enumerable = (target as IEnumerable);

            if (_settings.ProcessIEnumerableRecursively && enumerable != null)
            {
                foreach (object obj in enumerable)
                {
                    PSObject psobj = obj as PSObject;
                    if (psobj != null)
                    {
                        ProcessInputObjectInternal(psobj.BaseObject);
                    }
                    else
                    {
                        ProcessInputObjectInternal(obj);
                    }
                }
            }
            else
            {
                WriteError(PscxErrorRecord.InvalidInputError(_actions.Keys, target));
            }
        }

        private bool TryInvokeAction(Type type, object target)
        {
            if (_actions.ContainsKey(type))
            {
                Delegate action = _actions[type];

                if (action != null)
                {
                    action.DynamicInvoke(target);
                }

                return true;
            }

            return false;
        }
    }
}
